Skip to content

Track history session membership by entry instead of index - #1798

Merged
tleonhardt merged 2 commits into
mainfrom
fix-history-session-membership
Oct 31, 2026
Merged

tleonhardt merged 2 commits into
mainfrom
fix-history-session-membership

Conversation

@tleonhardt

Copy link
Copy Markdown
Member

Summary

Follow-up to #1795. A review of that change found that a single session_start_index can't describe the session once the list is changed in place. This PR changes how History tracks the session.

History now records which entries were present when start_session() ran. Unbounded ranges, str_search(), regex_search(), and the history --run/--edit --all guard all check that membership. Moving, filtering, or repeating entries therefore keeps persisted commands persisted and new entries current. This replaces the six list mutator overrides added in #1795.

Problems fixed

Using 4 persisted entries (o0–o3) followed by 2 current entries (n0, n1):

Operation Before After
reverse() current session = o1, o0 n1, n0
sort() ascending by raw current session = o2, o3 n0, n1
history *= 2 / history += list(history) copies of o0–o3 counted as current only n0, n1 copies are current
history --run 5:6 after reverse() replayed persisted commands without --all refused: "Use --all to run or edit commands from previous sessions."
history[:] = [history[1], new] right after start_session() new hidden as persisted new is current
history[:] = [n0, o1, n1] o1 counted as current o1 stays persisted

Behavior notes

  • New entries belong to the current session wherever they are inserted or assigned. Previously, a new entry placed before the old boundary was counted as persisted.
  • Membership belongs to the entry object. Appending the same persisted HistoryItem object again keeps it persisted. To record a persisted command again in the current session, append its statement (history.append(history[i].statement)). This is documented in History.is_persisted().
  • session_start_index is now a computed property: the index just past the last persisted entry. Every entry from that index on is current. Assigning it still marks the first n entries as persisted, so existing callers and tests that set it keep working.
  • Membership survives copy.deepcopy() and pickling. It is keyed by an identity wrapper rather than raw id() values, so copies and unpickled histories still recognize their persisted entries.
  • Adds public History.is_persisted(item).

Tests

  • tests/test_history_mutations.py was rewritten around a current_session() helper. It returns {command number: raw} and checks that span(":"), str_search(), and regex_search() agree. It also checks the session_start_index rule. Expected values are hand-derived literals.
  • New tests cover reverse/sort/*=/+=, assignment into an empty session, persisted entries placed after current ones, inserts at any position, re-appending a persisted object, deepcopy/pickle, and the --all guard after reordering and repetition. Each failed on main before the change.
  • Removed test_remove_retains_native_list_dispatch and test_failed_straddling_replacement_preserves_history, which tested the internals of the deleted overrides. Failed mutations are still covered by test_failed_mutation_preserves_history_and_membership.

Test plan

  • make check (ruff, prettier, typos, ty, mypy)
  • make test: 2924 passed, 3 skipped; cmd2/history.py at 100% coverage
  • make docs-test

History kept the current session as a single start index and overrode
each list mutator to keep it in place. That could not describe reordered
lists: reverse() and sort() were not overridden, repetition and some
slice assignments counted persisted copies as current, and the
history --run/--edit guard then replayed persisted commands without
--all.

History now records which entries were present when start_session()
ran. Unbounded ranges, searches, and the --all guard check that
membership, so moving, filtering, or repeating entries keeps persisted
commands persisted and new entries current. This replaces the list
mutator overrides. Membership survives copying and pickling.

session_start_index is now computed as the index just past the last
persisted entry and remains assignable. Add History.is_persisted().
@codecov

codecov Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.75%. Comparing base (226c3b7) to head (99d806e).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1798      +/-   ##
==========================================
- Coverage   99.75%   99.75%   -0.01%     
==========================================
  Files          23       23              
  Lines        6624     6592      -32     
==========================================
- Hits         6608     6576      -32     
  Misses         16       16              
Flag Coverage Δ
unittests 99.75% <100.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@tleonhardt tleonhardt self-assigned this Oct 31, 2026
@tleonhardt tleonhardt added the bug label Oct 31, 2026
Move the behavior changes from the History session membership fix out
of Bug Fixes. Entries added after start_session() are current wherever
they are placed, session_start_index is computed and its setter marks
only entries already in the list, and history[session_start_index:] is
unreliable after reordering, so is_persisted() is the check to use.
@tleonhardt
tleonhardt merged commit c58295f into main Oct 31, 2026
32 checks passed
@tleonhardt
tleonhardt deleted the fix-history-session-membership branch October 31, 2026 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant